﻿using OxyPlot.Axes;
using OxyPlot.Series;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace OxyPlot.CatExtension
{
    /// <summary>
    /// 使用Simd和并行计算加速渲染，不支持插值和BrokenLine，需要x坐标单调递增，使用AddPoint方法添加点
    /// </summary>
    public sealed class SimdLineSeries : XYAxisSeries
    {
        [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "UpdateActualMaxMin")]
        static extern void UpdateAxisActualMaxMin(Axis axis);
        [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "UpdateTransform")]
        static extern void UpdateAxisTransform(Axis axis, OxyRect bounds);

        public LineLegendPosition LineLegendPosition { get; set; } = LineLegendPosition.None;
        public double StrokeThickness { get; set; } = 2;
        public LineStyle LineStyle { get; set; } = LineStyle.Automatic;
        public LineStyle ActualLineStyle
        {
            get
            {
                return LineStyle;
            }
        }
        public OxyColor Color { get; set; } = OxyColors.Automatic;
        public OxyColor ActualColor
        {
            get
            {
                if (Color == OxyColors.Automatic)
                {
                    Color = PlotModel.GetDefaultColor();
                }
                return Color;
            }
        }
        public double[]? Dashes { get; set; }
        public double[]? ActualDashArray
        {
            get
            {
                return this.Dashes ?? this.ActualLineStyle.GetDashArray();
            }
        }
        public LineJoin LineJoin { get; set; } = LineJoin.Bevel;

        public MarkerType MarkerType { get; set; } = MarkerType.None;
        public int MarkerResolution { get; set; } = 0;
        public ScreenPoint[]? MarkerOutline { get; set; } = null;
        public double MarkerSize { get; set; } = 3;
        public OxyColor MarkerFill { get; set; } = OxyColors.Automatic;
        public OxyColor ActualMarkerFill
        {
            get
            {
                return MarkerFill == OxyColors.Automatic ? ActualColor : MarkerFill;
            }
        }
        public OxyColor MarkerStroke { get; set; } = OxyColors.Automatic;
        public double MarkerStrokeThickness { get; set; } = 2;


        private readonly List<ScreenPoint> decimatorBuffer = [];

        private readonly List<double> _pointsX = [];
        private readonly List<double> _pointsY = [];

        private readonly SimdDecimator decimator = new();

        public SimdLineSeries()
        {
            IsXMonotonic = true;
        }

        public void AddPoint(double x, double y)
        {
            _pointsX.Add(x);
            _pointsY.Add(y);
        }

        public void AddPoints(IEnumerable<double> x, IEnumerable<double> y)
        {
            _pointsX.AddRange(x);
            _pointsY.AddRange(y);
        }

        public void AddPoints(Span<double> x, Span<double> y)
        {
            _pointsX.AddRange(x);
            _pointsY.AddRange(y);
        }

        public void ClearPoints()
        {
            _pointsX.Clear();
            _pointsY.Clear();
        }

        public override void Render(IRenderContext rc)
        {
            if (_pointsX.Count == 0) return;

            this.VerifyAxes();

            var (minY, maxY) = Simd.MinMax(CollectionsMarshal.AsSpan(_pointsY));

            XAxis.Include(_pointsX[0]);
            XAxis.Include(_pointsX[^1]);
            YAxis.Include(minY);
            YAxis.Include(maxY);

            UpdateAxisActualMaxMin(XAxis);
            UpdateAxisActualMaxMin(YAxis);

            var rect = PlotModel.PlotArea;

            UpdateAxisTransform(XAxis, rect);
            UpdateAxisTransform(YAxis, rect);

            this.RenderPoints(rc);

            if (this.LineLegendPosition != LineLegendPosition.None && !string.IsNullOrEmpty(this.Title))
            {
                this.RenderLegendOnLine(rc);
            }
        }

        public void RenderPoints(IRenderContext rc)
        {
            var (start, end) = this.GetScreenDataRange();

            decimator.Decimate(_pointsX, _pointsY, start, end, XAxis.Offset, XAxis.Scale, YAxis.Offset, YAxis.Scale, this.decimatorBuffer);

            this.RenderLineAndMarkers(rc, this.decimatorBuffer);
            decimatorBuffer.Clear();
        }

        /// <summary>
        /// Renders a legend on the line.
        /// </summary>
        /// <param name="rc">The render context.</param>
        public void RenderLegendOnLine(IRenderContext rc)
        {
            // Find the position
            int point;
            HorizontalAlignment ha;
            var va = VerticalAlignment.Middle;
            double dx = 4;

            switch (this.LineLegendPosition)
            {
                case LineLegendPosition.Start:
                    point = 0;
                    ha = HorizontalAlignment.Right;
                    dx = -dx;
                    break;
                case LineLegendPosition.End:
                    point = _pointsX.Count - 1;
                    ha = HorizontalAlignment.Left;
                    break;
                case LineLegendPosition.None:
                    return;
                default:
                    throw new ArgumentOutOfRangeException();
            }

            this.Orientate(ref ha, ref va);
            var pt = this.Transform(_pointsX[point], _pointsY[point]) + this.Orientate(new ScreenVector(dx, 0));

            // Render the legend
            rc.DrawText(
                pt,
                this.Title,
                this.ActualTextColor,
                this.ActualFont,
                this.ActualFontSize,
                this.ActualFontWeight,
                0,
                ha,
                va);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public ScreenPoint Transform(double x, double y)
        {
            return new ScreenPoint(XAxis.Transform(x), YAxis.Transform(y));
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private (int, int) GetScreenDataRange()
        {
            var span = CollectionsMarshal.AsSpan(_pointsX);

            var xmin = this.XAxis.ClipMinimum;
            var xmax = this.XAxis.ClipMaximum;

            var start = BinarySearch(span, xmin);
            var end = BinarySearch(span, xmax);

            return (Math.Max(start - 1, 0), Math.Min(end + 1, _pointsX.Count - 1));
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static int BinarySearch(Span<double> points, double x)
        {
            int left = 0, mid = 0, right = points.Length - 1;

            if (x <= points[left])
            {
                return left;
            }

            if (points[right] <= x)
            {
                return right;
            }

            while (left <= right)
            {
                mid = (left + right) / 2;
                var result = x - points[mid];

                if (result < 0)
                {
                    right = mid - 1;
                }
                else
                {
                    left = mid + 1;
                }
            }
            return mid;
        }

        /// <summary>
        /// Renders the transformed points as a line (smoothed if <see cref="InterpolationAlgorithm"/> isn’t <c>null</c>) and markers (if <see cref="MarkerType"/> is not <c>None</c>).
        /// </summary>
        /// <param name="rc">The render context.</param>
        /// <param name="pointsToRender">The points to render.</param>
        private void RenderLineAndMarkers(IRenderContext rc, List<ScreenPoint> pointsToRender)
        {
            var screenPoints = pointsToRender;

            // clip the line segments with the clipping rectangle
            if (this.StrokeThickness > 0 && this.LineStyle != LineStyle.None)
            {
                this.RenderLine(rc, screenPoints);
            }

            if (this.MarkerType != MarkerType.None)
            {
                var markerBinOffset = this.MarkerResolution > 0 ? this.Transform(this.MinX, this.MinY) : default(ScreenPoint);

                rc.DrawMarkers(
                    pointsToRender,
                    this.MarkerType,
                    this.MarkerOutline,
                    [this.MarkerSize],
                    this.ActualMarkerFill,
                    this.MarkerStroke,
                    this.MarkerStrokeThickness,
                    this.EdgeRenderingMode,
                    this.MarkerResolution,
                    markerBinOffset);
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private void RenderLine(IRenderContext rc, List<ScreenPoint> pointsToRender)
        {
            var dashArray = this.ActualDashArray;

            DrawReducedLine(
                rc,
                pointsToRender,
                this.GetSelectableColor(this.ActualColor),
                this.StrokeThickness,
                this.EdgeRenderingMode,
                dashArray,
                this.LineJoin);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static void DrawReducedLine(IRenderContext rc, List<ScreenPoint> points, OxyColor stroke, double strokeThickness, EdgeRenderingMode edgeRenderingMode, double[]? dashArray, LineJoin lineJoin)
        {
            rc.DrawLine(points, stroke, strokeThickness, edgeRenderingMode, dashArray, lineJoin);
        }


        public override TrackerHitResult? GetNearestPoint(ScreenPoint point, bool interpolate)
        {
            if (XAxis == null || YAxis == null || _pointsX.Count == 0)
            {
                return null;
            }

            TrackerHitResult? result;
            if (interpolate)
            {
                result = GetNearestInterpolatedPoint(point);
            }
            else
            {
                result = GetNearestPoint(point);
            }

            if (result != null)
            {
                result.Text = StringHelper.Format(
                    this.ActualCulture,
                    this.TrackerFormatString,
                    result.Item,
                    this.Title,
                    this.XAxis.Title ?? XYAxisSeries.DefaultXAxisTitle,
                    this.XAxis.GetValue(result.DataPoint.X),
                    this.YAxis.Title ?? XYAxisSeries.DefaultYAxisTitle,
                    this.YAxis.GetValue(result.DataPoint.Y));
            }

            return result;

        }

        private TrackerHitResult? GetNearestInterpolatedPoint(ScreenPoint screenPoint)
        {
            var dataPoint = InverseTransform(screenPoint);

            var xSpan = CollectionsMarshal.AsSpan(_pointsX);
            var ySpan = CollectionsMarshal.AsSpan(_pointsY);

            var p1Index = BinarySearch(xSpan, dataPoint.X);

            var p2Index = 0;

            if (p1Index == 0 || p1Index == xSpan.Length - 1)
            {
                p2Index = p1Index;
            }
            else if (xSpan[p1Index] < dataPoint.X)
            {
                p2Index = p1Index + 1;
            }
            else
            {
                p2Index = p1Index - 1;
            }

            double dx, dy;

            if (p1Index == p2Index)
            {
                dx = xSpan[p1Index];
                dy = ySpan[p1Index];
            }
            else
            {
                var k1 = (ySpan[p2Index] - ySpan[p1Index]) / (xSpan[p2Index] - xSpan[p1Index]);
                var b1 = ySpan[p1Index] - k1 * xSpan[p1Index];

                var k2 = -1 / k1;
                var b2 = dataPoint.Y - k2 * dataPoint.X;

                dx = (b2 - b1) / (k1 - k2);
                dy = k1 * dx + b1;
            }

            dataPoint = new DataPoint(dx, dy);
            screenPoint = Transform(dataPoint);

            var index = Math.Min(p1Index, p2Index);

            return new TrackerHitResult
            {
                Series = this,
                DataPoint = dataPoint,
                Position = screenPoint,
                Index = index,
                Item = new DataPoint(xSpan[index], ySpan[index])
            };
        }

        private TrackerHitResult? GetNearestPoint(ScreenPoint screenPoint)
        {
            var dataPoint = InverseTransform(screenPoint);

            var xSpan = CollectionsMarshal.AsSpan(_pointsX);

            var p1Index = BinarySearch(xSpan, dataPoint.X);

            var p2Index = 0;

            if (p1Index == 0 || p1Index == xSpan.Length - 1)
            {
                p2Index = p1Index;
            }
            else if (xSpan[p1Index] < dataPoint.X)
            {
                p2Index = p1Index + 1;
            }
            else
            {
                p2Index = p1Index - 1;
            }

            var index = Math.Min(p1Index, p2Index);

            dataPoint = new DataPoint(xSpan[index], _pointsY[index]);
            screenPoint = Transform(dataPoint);

            return new TrackerHitResult
            {
                Series = this,
                DataPoint = dataPoint,
                Position = screenPoint,
                Index = index,
                Item = dataPoint
            };
        }
    }
}
